Go 语言学习-反射
一般的编程语言都内置了反射工具,Go 虽然是面向过程的语言,但是也提供了这个工具包
静态类型与动态类型
变量中的类型,可以分为静态类型和具体类型(动态类型)
反射取得的就是这个 pair
func main() {
var a string
// pair<statictype:string, value: "张三">
a = "张三"
var aty interface{}
// pair<type:string, value: "张三">
aty = a
str, _ := aty.(string)
fmt.Println(str)
}
静态类型
所谓的静态类型(即 static type),就是变量声明的时候的类型。(在编码时,肉眼可见的类型)
var age int // int 是静态类型
var name string // string 也是静态类型
动态类型
所谓的 动态类型(即 concrete type,也叫具体类型)是 程序运行时系统才能看见的类型。例如空接口这种,什么 int 呀,string 呀,都可以接收。
var i interface{}
i = 18
i = "张三"
第一行:我们在给 i 声明了 interface{}
类型,所以 i 的静态类型就是 interface{}
第二行:当我们给变量 i 赋一个 int 类型的值时,它的静态类型还是 interface{}
,这是不会变的,但是它的动态类型此时变成了 int 类型。
第三行:当我们给变量 i 赋一个 string 类型的值时,它的静态类型还是 interface{}
,它还是不会变,但是它的动态类型此时又变成了 string 类型。
反射的常用方法
这里介绍下常用的反射方法
reflect.Elem()
reflect.Elem()
函数,通过反射获取指针指向的元素类型
注意:这里侧重的是类型信息,即那些字段名,类型之类的
Go 语言程序中对指针获取反射对象时,可以通过 reflect.Elem()
方法获取这个指针指向的元素类型。这个获取过程被称为取元素,等效于对指针类型变量做了一个 *
操作,代码如下:
package main
import (
"fmt"
"reflect"
)
func main() {
// 声明一个空结构体
type cat struct {
}
// 创建cat的实例
ins := &cat{}
// 获取结构体实例的反射类型对象
typeOfCat := reflect.TypeOf(ins)
// 显示反射类型对象的名称和种类
fmt.Printf("name:'%v' kind:'%v'\n",typeOfCat.Name(), typeOfCat.Kind())
// 输出指针变量的类型名称和种类。
// Go 语言的反射中对所有指针变量的种类都是 Ptr,但注意,指针变量的类型名称是空,不是 *cat。
// 取类型的元素
typeOfCat = typeOfCat.Elem()
// 取指针类型的元素类型,也就是 cat 类型。这个操作不可逆,不可以通过一个非指针类型获取它的指针类型。
// 显示反射类型对象的名称和种类
fmt.Printf("element name: '%v', element kind: '%v'\n", typeOfCat.Name(), typeOfCat.Kind())
}
输出内容:
name: '' kind: 'ptr'
element name: 'cat', element kind: 'struct'
示例2
type Book struct {
Id int
Title string
Price float32
Authors []string
}
// Main function
func main() {
book := Book{}
//use of Elem() method
e := reflect.ValueOf(&book).Elem()
for i := 0; i < e.NumField(); i++ {
varName := e.Type().Field(i).Name
varType := e.Type().Field(i).Type
varValue := e.Field(i).Interface()
fmt.Printf("%v %v %v\n", varName, varType, varValue)
}
}
输出内容:
Id int 0
Title string
Price float32 0
Authors []string []
reflect.Value ⭐
Go 语言中,使用 reflect.ValueOf()
函数获得值的反射值对象(reflect.Value)
value := reflect.ValueOf(rawValue)
从反射值对象获取被包装的值
可以通过下面几种方法从反射值对象 reflect.Value
中获取原值,如下表所示。
方法名 | 说明 |
---|---|
Interface() interface {} | 将值以 interface{} 类型返回,可以通过类型断言转换为指定类型 |
Int() int64 | 将值以 int 类型返回,所有有符号整型均可以此方式返回 |
Uint() uint64 | 将值以 uint 类型返回,所有无符号整型均可以此方式返回 |
Float() float64 | 将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回 |
Bool() bool | 将值以 bool 类型返回 |
Bytes() []bytes | 将值以字节数组 []bytes 类型返回 |
String() string | 将值以字符串类型返回 |
反射包 reflect 实例
上文说到,变量里面有两个内部的熟悉,一个是 type 一个是 value,可以通过反射取得这两个值
import (
"fmt"
"reflect"
)
func reflectNum(arg interface{}) {
fmt.Println("type: ", reflect.TypeOf(arg))
fmt.Println("value: ", reflect.ValueOf(arg))
}
func main() {
reflectNum(100)
}
通过反射取得方法和字段
// go不能通过反射修改私有变量,也不能通过反射调用私有方法。
type User struct {
Id int
Name string
Age int
}
// 注意这里是 User 类型 不是 *User 类型,在结构体当中,接收者类型的区别将影响该结构体方法的可见性。
func (re User) Call() {
fmt.Println("User is called...")
fmt.Printf("%v \n", re)
}
func main() {
user := User{1, "Aceld", 18}
DoFiledAndMethod(user)
}
func DoFiledAndMethod(input interface{}) {
// 获取基本的类型和 value
inputType := reflect.TypeOf(input) // 返回 Type 类型
fmt.Println("inputType is :", inputType.Name())
inputValue := reflect.ValueOf(input)
fmt.Println("inputValue is :", inputValue)
fmt.Println("===============================")
// 通过 type 取得里面的字段
for i := 0; i < inputType.NumField(); i++ {
field := inputType.Field(i)
value := inputValue.Field(i).Interface()
fmt.Println(field.Name, value)
}
fmt.Println("===============================")
// 通过 type 取得里面的方法并调用
for i := 0; i < inputType.NumMethod(); i++ {
m := inputType.Method(i)
fmt.Println(m.Name, m.Type)
}
}
注意,只有值方法才能返回数目,指针方法不统计
使用 Tag 标签
这个标签和 Java 中的注解作用是一样的,都是反射时获取
type Human struct {
Name string `info:"name" doc:"名字"`
Sex string `info:"性别"`
}
这个标签以 key-value 形式,使用空格分隔,可以给字段添加任意数量的 Tag
type Human struct {
Name string `info:"name" doc:"名字"`
Sex string `info:"性别"`
}
func findTag(arg interface{}) {
t := reflect.TypeOf(arg)
for i := 0; i < t.NumField(); i++ {
tag := t.Field(i).Tag.Get("info")
fmt.Println("info: ", tag)
}
}
func main() {
findTag(Human{"张三", "女"})
}
反射创建对象
快速创建一个实例
type A struct {
Name string
}
func (a *A) String() string {
return fmt.Sprintf("Hello %s \n", a.Name)
}
// 测试 unit
func main() {
val := reflect.New(reflect.TypeOf(A{})).Interface()
v := val.(*A)
fmt.Println(v.String())
}
type A struct {
Name string
}
// 测试 unit
func TestReflect(t *testing.T) {
reflectNew((*A)(nil))
}
//反射创建新对象。
func reflectNew(target interface{}) {
if target == nil {
fmt.Println("参数不能未空")
return
}
t := reflect.TypeOf(target)
if t.Kind() == reflect.Ptr { //指针类型获取真正 type 需要调用 Elem
t = t.Elem()
}
newStruc := reflect.New(t)// 调用反射创建对象
newStruc.Elem().FieldByName("Name").SetString("Lily") //设置值
newVal := newStruc.Elem().FieldByName("Name") //获取值
fmt.Println(newVal.String())
}
深度比较 DeepEqual
对于 array、slice、map、struct 等类型,想要比较两个值是否相等,不能使用 ==
,处理起来十分麻烦,在对效率没有太大要求的情况下,reflect 包中的 DeepEqual 函数完美的解决了比较问题。
func DeepEqual(a1, a2 interface{}) bool
示例:
func main() {
m1 := map[int]interface{}{1: []int{1, 2, 3}, 2: 3, 3: "a"}
m2 := map[int]interface{}{1: []int{1, 2, 3}, 2: 3, 3: "a"}
if reflect.DeepEqual(m1, m2) {
fmt.Println("相等")
}
}
读取标签并赋值示例
下面利用反射实现一个简单的功能,来看看反射如何帮助我们简化代码的。
假设有一个配置类 Config,每个字段是一个配置项。为了简化实现,假设字段均为 string 类型:
type Config struct {
Name string `json:"server-name"`
IP string `json:"server-ip"`
URL string `json:"server-url"`
Timeout string `json:"timeout"`
}
配置默认从 json 文件中读取,如果环境变量中设置了某个配置项,则以环境变量中的配置为准。配置项和环境变量对应的规则非常简单:将 json 字段的字母转为大写,将 -
转为下划线,并添加 CONFIG_
前缀。
最终的对应结果如下:
type Config struct {
Name string `json:"server-name"` // CONFIG_SERVER_NAME
IP string `json:"server-ip"` // CONFIG_SERVER_IP
URL string `json:"server-url"` // CONFIG_SERVER_URL
Timeout string `json:"timeout"` // CONFIG_TIMEOUT
}
实现这个功能非常简单,使用 switch case
或者 if else
硬编码很快就搞定了。但是,如果使用硬编码,Config 结构发生改变,例如修改 json 对应的字段,删除或新增了一个配置项,这块逻辑也需要发生改变。而更大的问题在于:容易出错,不好测试!!!
这个时候,就有了 reflect 的用武之地了。
func readConfig() *Config {
// read from xxx.json,省略
config := Config{}
typ := reflect.TypeOf(config)
value := reflect.Indirect(reflect.ValueOf(&config))
for i := 0; i < typ.NumField(); i++ {
f := typ.Field(i)
if v, ok := f.Tag.Lookup("json"); ok {
key := fmt.Sprintf("CONFIG_%s", strings.ReplaceAll(strings.ToUpper(v), "-", "_"))
if env, exist := os.LookupEnv(key); exist {
value.FieldByName(f.Name).Set(reflect.ValueOf(env))
}
}
}
return &config
}
func main() {
os.Setenv("CONFIG_SERVER_NAME", "global_server")
os.Setenv("CONFIG_SERVER_IP", "10.0.0.1")
os.Setenv("CONFIG_SERVER_URL", "example.com")
c := readConfig()
fmt.Printf("%+v", c)
}
实现逻辑其实是非常简单的:
- 在运行时,利用反射获取到 Config 的每个字段的 Tag 属性,拼接出对应的环境变量的名称。
- 查看该环境变量是否存在,如果存在,则将环境变量的值赋值给该字段。
运行该程序,输出为:
&{Name:global_server IP:10.0.0.1 URL:example.com Timeout:}
可以看到,环境变量中设置的三个配置项已经生效。之后无论结构体 Config 内部的字段发生任何改变,这部分代码无需任何修改即可完美的适配,出错概率也极大地降低。
使用反射修改私有变量
可以使用反射(reflection)来修改私有变量。在 Go 语言中,反射是一种强大的机制,允许在运行时检查变量的类型和值,并且可以修改私有字段的值。但是需要注意的是,反射通常不是推荐的做法,因为它会带来一些运行时的性能开销和代码可读性的降低。除非必要,最好避免在正常情况下使用反射来修改私有变量。
下面是一个示例代码,演示如何使用反射修改私有变量:
package main
import (
"fmt"
"reflect"
)
type Person struct {
name string
age int
salary float64
}
func main() {
p := Person{"John", 30, 50000.0}
// 使用反射获取 p 的值的反射对象
personReflect := reflect.ValueOf(&p).Elem()
// 获取私有字段的反射对象
// 在 Go 中,私有字段的名称会被编译器修改为包名+类型名+字段名
// 在这里,私有字段 name 被重命名为 main.Person.name
privateField := personReflect.FieldByName("name")
// 判断是否找到了私有字段
if privateField.IsValid() && privateField.CanSet() {
// 修改私有字段的值
privateField.SetString("Alice")
}
fmt.Println("Modified Person:", p)
}
在上述代码中,我们定义了一个 Person
结构体,其中的字段 name
是私有字段。使用反射,我们可以获取 Person
结构体的反射值,并通过 FieldByName
方法找到并修改私有字段 name
的值。请注意,这里仅仅是为了演示反射的使用,实际代码中应该避免过度使用反射来修改私有字段,以保持代码的健壮性和可读性。